"""
# Make Version using python2 on Linux.
# Usage:
#     e.g. python2 tools/scripts/make_version.py
#     e.g. python2 tools/scripts/make_version.py debug
# Notice:
#     when released with lib, DO NOT use this script to get a release tar.
"""
from __future__ import print_function
import os
import sys
import time
import datetime

print(__doc__)
debug = 0  # when set as non-zero, show debug info
print_number = 1  # when set as non-zero, show line no. for a list

"""external configuration"""
path_root = 'Huawei_LiteOS'
dir_root_readonly = 'Huawei_LiteOS'
dir_version_readonly = 'ReleaseNotes'
path_platform_version = 'platform/bsp/common/los_config.h'
path_release_notes = 'tools/build/ReleaseNotes'
str_version_readonly = 'Huawei LiteOS'
# for version detected in los_config.h
str_version_internal = '#define HW_LITEOS_VERSION'
str_version_external = ['#define MAJ_V', '#define MIN_V', '#define REL_V']
remove_list = [
    '/vendor',
    '/3rdParty',
    '/components',
    '/net/wpa_supplicant',
    '/platform/bsp/board/hi3731',
    '/drivers/usb/gadget/dfu',
    '/drivers/usb/gadget/f_dfu.c',
    '/drivers/usb/gadget/f_dfu.h',
    '/drivers/usb/gadget/g_dnl.c',
    '/drivers/usb/gadget/g_dnl.h',
    '/drivers/usb/gadget/f_ether.c',
    '/drivers/usb/gadget/f_ether.h',
    '/drivers/usb/gadget/u_ether.c',
    '/drivers/usb/gadget/u_ether.h',
    '/drivers/usb/gadget/f_rndis.c',
    '/drivers/usb/gadget/f_rndis.h',
    '/drivers/usb/gadget/rndis.c',
    '/drivers/usb/gadget/rndis.h',
    '/drivers/usb/gadget/skbuff.c',
    '/drivers/usb/gadget/skbuff.h',
    '/drivers/usb/gadget/f_serial.c',
    '/drivers/usb/gadget/f_serial.h',
    '/drivers/usb/gadget/f_acm.c',
    '/drivers/usb/gadget/f_acm.h',
    '/drivers/usb/gadget/u_serial.c',
    '/drivers/usb/gadget/u_serial.h',
    '/drivers/usb/gadget/tty_serial.c',
    '/drivers/usb/gadget/tty_serial.h',
    '/drivers/usb/gadget/f_multi.c',
]

modules = [
    'compat',
    'build',
    'doc',
    'drivers',
    'fs',
    'kernel',
    'lib',
    'net',
    'platform',
    'sample',
    'shell',
    'tools',
    '3rdParty',
]

# commit messages in these git repos will be collected
git_repos_for_commits = [
    'compat/cmsis',
    'compat/linux',
    'compat/posix',
    'doc',
    'drivers/gpio',
    'drivers/hidmac',
    'drivers/hieth-sf',
    'drivers/higmac',
    'drivers/i2c',
    'drivers/mem',
    'drivers/mmc',
    'drivers/mtd/common',
    'drivers/mtd/nand',
    'drivers/mtd/spi_nor',
    'drivers/random',
    'drivers/rtc',
    'drivers/spi',
    'drivers/uart',
    'drivers/usb',
    'drivers/video',
    'drivers/wifi',
    'drivers/wtdg',
    'fs/fat',
    'fs/include',
    'fs/jffs2',
    'fs/nfs',
    'fs/proc',
    'fs/ramfs',
    'fs/vfs',
    'fs/yaffs2',
    'kernel',
    'lib',
    'net',
    'platform/bsp',
    'platform/cpu',
    'sample',
    'shell',
    'tools',
]

# modified lines in these git repos will be collected
git_repos_for_line = [
    # 'build',
    'compat/cmsis',
    'compat/linux',
    'compat/posix',
    'drivers/gpio',
    'drivers/hidmac',
    'drivers/hieth-sf',
    'drivers/higmac',
    'drivers/i2c',
    'drivers/mem',
    'drivers/mmc',
    'drivers/mtd/common',
    'drivers/mtd/nand',
    'drivers/mtd/spi_nor',
    'drivers/random',
    'drivers/rtc',
    'drivers/spi',
    'drivers/uart',
    'drivers/usb',
    'drivers/video',
    'drivers/wifi',
    'drivers/wtdg',
    'fs/fat',
    'fs/include',
    'fs/jffs2',
    'fs/nfs',
    'fs/proc',
    'fs/ramfs',
    'fs/vfs',
    'fs/yaffs2',
    'kernel',
    'lib',
    'net',
    'platform/bsp',
    'platform/cpu',
    'sample',
    'shell',
    # 'test',
    # 'tools',
]
bugfix = 'bugfix'
feature = 'Feature'
all_release_notes = {bugfix: {}, feature: {}}
str_dts_prefixes = [
    '    No.:',
    '    Bug Type:',
    '    Module name:',
    '',  # for actual module name
    '',  # for actual git repo
    '    Modify reason:',
    '    Modify content:',
    '    Submitter:',
]
str_dts_prefixes_len = len(str_dts_prefixes)
commit_min_line_count = 13

"""internal configuration"""
path_root_parent = ''  # e.g. /xxx/mainline/
path_archive_tmp_root = ''
version_internal = ''  # e.g. V2R2C00B001
version_external = [-1, -1, -1]  # e.g. [1,0,1]
version_internal_new = ''
version_external_new = [-1, -1, -1]
last_release_date = [-1, -1, -1]  # will be updated to str
now_release_date = [-1, -1, -1]  # will be updated to str

menus = [
    """Show menu""",
    """Update version No. and ReleaseNote""",
    """Archive for test/release   (remove modules: vendor, 3rdParty, components...)""",
    #"""Archive for release  (remove modules: vendor, 3rdParty, test)""",  # use above for release
    """Count modified code line""",
    """Exit"""]
all_commits = []
all_DTS_AR = []

"""shell commands"""
cmd_repo_sync = 'make dynloadclean; make clean; make cleanall; repo sync; cp -f {0}tools/build/ReleaseNotes {0}ReleaseNotes'
cmd_repo_status = 'repo status'
# common: path_root, dir_root_readonly, path_archive_tmp_root
cmds_archive_common_before = [
    'mkdir {2};cp -rf {0} {2}{1}',
    'rm -rf ' + ' '.join(['{2}{1}' + f for f in remove_list]),
    '''sed -i '/vendor\/Kconfig"/d' {2}{1}/tools/menuconfig/extra/Configs/Config.in''',
    '''sed -i '/3rdParty\/Kconfig"/d' {2}{1}/tools/menuconfig/extra/Configs/Config.in''',
    'find {2}{1} -name *.git | xargs rm -rf', ]
cmds_archive_common_after = [
    'rm -rf {2}', ]
# archive: path_archive_tmp_root, version_internal_new, dir_root_readonly, path_root_parent
cmds_archive_test = [
    'cd {0};echo Start to tar -zcf {3}LiteOS-{1}.tar.gz for test...; tar -zcf {3}LiteOS-{1}.tar.gz {2}']
cmds_archive_release = [
    'rm -rf {0}{2}/test',
    '''sed -i '/test\/Kconfig"/d' {0}{2}/tools/menuconfig/extra/Configs/Config.in''',
    'cd {0};echo Start to tar -zcf {3}LiteOS-{1}.tar.gz for release...; tar -zcf {3}LiteOS-{1}.tar.gz {2}']


def print_menu(menu):
    print('{0} {1} {0}'.format('=' * 20, menu))


def print_debug(str):
    if not debug:
        return
    print('{:10}{}'.format('[DEBUG]', str))


def print_debug_list(menu_msg, list_src):
    if not debug:
        return
    print_list(menu_msg, list_src)


def print_list(menu_msg, list_src):
    print('')
    print_menu(menu_msg.format(len(list_src)))
    for n, t in enumerate(list_src):
        print_no(n)
        print(t)
    print('')


def print_no(n):
    if print_number:
        print('{}. '.format(n + 1), end='')


def os_popen(str, mode='r'):
    print_debug(str)
    os.popen(str, mode)


def get_option():
    n = raw_input('Please enter your choice(1-{}): '.format(len(menus)))
    if len(n.replace('\r', '').replace('\r', '').strip()) == 0:
        return -1
    m = n
    try:
        n = int(n) - 1
        assert n >= 0 and n < len(menus)
    except ValueError:
        print('{:10}Invalid input: {}\n'.format('[Error]', m))
        n = -1
    return n


def show_menu():
    print_menu('menu')
    for i in range(len(menus)):
        print('{:}\t{:}'.format(i + 1, menus[i]))
    print_menu('menu')
    print('')


def get_version():
    """
    When get version, you should use info in tools/build/ReleaseNotes, NOT root/ReleaseNotes.
    Because the update sequence is: los_config.h ->  tools/.../ReleaseNotes -> root/ReleaseNotes.
    :return:
    """
    global version_external, version_internal
    global version_external_new, version_internal_new
    # get old version
    flag_get = 0
    for line in open(path_root + path_platform_version, 'r'):
        if line.find(str_version_internal) > -1:
            version_internal = line.split()[-1][1:-1]
            flag_get += 1
        for i in range(len(str_version_external)):
            if line.find(str_version_external[i]) > -1:
                version_external[i] = int(line.split()[-1])
                flag_get += 1
        if path_root == 4:
            break
    version_internal_new = version_internal
    for i in range(len(version_external)):
        version_external_new[i] = version_external[i]


def update_version():
    global version_external_new, version_internal_new
    version_internal_infer = ''
    version_external_infer = [-1, -1, -1]
    version_external_len = len(version_external)
    get_version()

    # infer new version
    version_internal_infer = version_internal[:-3] + '{:03d}'.format(int(version_internal[-3:]) + 1)
    for i in range(version_external_len - 1):
        version_external_infer[i] = version_external[i]
    version_external_infer[version_external_len - 1] = version_external[version_external_len - 1] + 1

    # let user decide the new version
    version_old = version_internal + ' ' + '.'.join(map(str, version_external))
    version_infer = version_internal_infer + ' ' + '.'.join(map(str, version_external_infer))
    while 1:
        print('{:32}{}'.format('Old version is', version_old))
        y = raw_input('{0:32}{1}\n{2:32}{3}\n{4:32}{3}, input: '.format('SO input \'n\' to use', version_old,
                                                                 'OR input \'y\' or nothing to use', version_infer,
                                                                 'OR input new version like'))
        x = y
        if len(y.strip()) == 0 or y.strip().replace('\'', '').lower() == 'y':
            version_internal_new = version_internal_infer
            for i in range(version_external_len):
                version_external_new[i] = version_external_infer[i]
            break
        elif y.strip().replace('\'', '').lower() == 'n' or y.strip().replace('\'', '').lower() == 'q':
            break
        y = y.replace('  ', ' ').replace('\'', '').strip().split()
        if len(y) != 2 or len(y[1].split('.')) != version_external_len:
            print('{:10}Invalid input: {}\n'.format('[Error]', x))
            continue
        version_internal_new = y[0].strip().replace('\'', '')
        v = y[1].strip().split('.')
        for i in range(version_external_len):
            version_external_new[i] = v[i]
        break
    print('{:15}\t{} {}\n'.format('New version is', version_internal_new, '.'.join(map(str, version_external_new))))

    # update version in los_config.h
    os_popen('''sed -i 's/{}/{}/g' {}'''.format(version_internal, version_internal_new,
                                                path_root + path_platform_version))
    for i in range(version_external_len):
        os_popen('''sed -i 's/\({}\){}/\\1{}/' {}'''.format(str_version_external[i] + '.*', version_external[i],
                                                            version_external_new[i], path_root + path_platform_version))


def get_release_date(skip_versions=0):
    """
    get_last_release_date and get_date_now
    :return:
    """
    global last_release_date
    if last_release_date[0] > 0:
        return
    for line in open(dir_version_readonly, 'r').readlines()[1:]:
        if line.startswith(str_version_readonly) and len(line.split()) > 3:
            if skip_versions:
                skip_versions -= 1
                continue
            last_release_date = map(int, line.split()[-1].split('/'))
            da = datetime.date(last_release_date[0], last_release_date[1], last_release_date[2])
            da += datetime.timedelta(days=1)
            last_release_date[0] = '{:04d}'.format(da.year)
            last_release_date[1] = '{:02d}'.format(da.month)
            last_release_date[2] = '{:02d}'.format(da.day)
            break
    localtime = time.localtime(time.time())
    now_release_date[0] = '{:04d}'.format(localtime.tm_year)
    now_release_date[1] = '{:02d}'.format(localtime.tm_mon)
    now_release_date[2] = '{:02d}'.format(localtime.tm_mday)

    print('{:20}: {}'.format('last_release_date+1', '-'.join(last_release_date)))
    print('{:20}: {}'.format('now_release_date', '-'.join(now_release_date)))
    # last_release_date = ['2018', '05', '01']


def add_commits(commits, git_repo):
    global all_commits
    for commit in commits:
        i = 0
        buf = ''
        t = ['' for _ in str_dts_prefixes]
        lines = commit.split('\n')
        if len(lines) < commit_min_line_count:
            continue
        for line in lines:
            if line.startswith(str_dts_prefixes[i]):
                if i != 0 and i != 6:
                    t[i - 1] = buf
                else:
                    t[i - 1] = buf[len(str_dts_prefixes[i - 1].strip()):].strip()
                buf = line[len(str_dts_prefixes[i]):].strip()
                if i == 3:
                    t[i] = git_repo.split('/')[0]
                    t[i + 1] = git_repo
                    i += 2
                i += 1
            else:
                buf += '\r{}'.format(' ' * 8 + line[4:].strip())
            if i == str_dts_prefixes_len:
                t[i - 1] = line[len(str_dts_prefixes[i - 1]):].strip()
                all_commits.append(t)
                break


def save_release_notes():
    """
    save to file, make sure ReleaseNotes have not been updated
    :return:
    """
    version_index = 0
    old_lines = open(path_root + path_release_notes, 'r').readlines()
    for line in old_lines[1:]:
        version_index += 1
        if line.startswith(str_version_readonly):
            version_release_notes = line.split()[2]
            break
    if version_release_notes == version_internal_new:
        print('{:10}{}'.format('[WARNING]', 'Skip update ReleaseNotes! Because the version ' +
                               'in ReleaseNotes is the same as new version: {}'.format(version_internal_new)))
        return
    str_bugfix_feature = [[feature, 'New Features\nThe following are some new features with this release.\n', 0],
                          [bugfix, 'Bug Fixs\nThe following are some bug fixs with this release.\n', 0]]
    for i, tup in enumerate(str_bugfix_feature):
        # if len(all_release_notes[tup[0]]) > 0:
        for module, commits in all_release_notes[tup[0]].items():
            if len(commits):
                for modify_content in commits:
                    str_bugfix_feature[i][2] += 1
    with open(path_root + path_release_notes, 'w') as f:
        for i in range(version_index):
            f.write(old_lines[i])
        f.write('{} {} {}{}\n'.format(str_version_readonly, version_internal_new, ' ' * 2, '/'.join(now_release_date)))
        f.write('{} {}\n{}\n'.format(str_version_readonly, '.'.join(map(str, version_external_new)), '-' * 10))
        for i, tup in enumerate(str_bugfix_feature):
            # if len(all_release_notes[tup[0]]) > 0:
            if i == 1 and str_bugfix_feature[0][2] != 0:
                f.write('\n')
            if str_bugfix_feature[i][2]:
                f.write(tup[1])
            for module, commits in all_release_notes[tup[0]].items():
                if len(commits):

                    f.write('\n{}* {}\n'.format(' ' * 4, module))
                    for modify_content in commits:
                        f.write('{}- {}\n'.format(' ' * 6, modify_content))
        f.write('\n')
        for i in range(len(old_lines) - version_index):
            f.write(old_lines[i + version_index])


def validate_all_commits():
    """
    Make sure feature commits start with feature number, NOT DTS.
    Delete '[test]' commits.
    Extract the first commit message for each DTS/AR.
    :return:
    """
    remove_test = 1
    global all_release_notes, all_commits
    illegal_feature = []
    for t in all_commits:
        if t[1] == feature and t[0][0] != 'A':
            illegal_feature.append(t)
    if len(illegal_feature):
        print_list('[WARNING] Found illegal commit messages! '
                   'These \'Feature\' will be recognized as \'Bugfix!\'.' +
                   'You SHOULD search these DTS/AR NO. on Gerrit:', illegal_feature)
    print_debug_list('All DTS and AR commits (Total: {}) are:', all_commits)

    # delete '[test]'
    if remove_test:
        truncated_commits = [t for t in all_commits if t[0][0] == 'A' or t[0][0] == 'D']
        all_commits = truncated_commits

    # sort commits by DTS/AR
    truncated_commits = sorted(all_commits, key=lambda x: x[0])
    all_commits = truncated_commits

    # Extract the first commit message for each DTS/AR.
    valid_release_notes = []
    for i, t in enumerate(all_commits):
        t_last = [x for x in t]
        if i + 1 == len(all_commits) or t[0] != all_commits[i + 1][0]:
            valid_release_notes.append(t_last)
            all_DTS_AR.append(t[0])
    print_debug_list('All valid ReleaseNotes (Total: {}) are:', valid_release_notes)
    print_debug_list('All DTS and AR NO. (Total: {})  are:', all_DTS_AR)
    for t in valid_release_notes:
        if t[1] == feature:
            all_release_notes[feature][t[3]].append(t[6])
        else:
            all_release_notes[bugfix][t[3]].append(t[6])


def collect_commit():
    get_release_date()
    # collect every commit in each git repo
    for git_repo in git_repos_for_commits:
        cmds = 'cd {}; git log --since={}'.format(path_root + git_repo, '-'.join(last_release_date))
        print_debug(cmds)
        logs = os.popen(cmds).read()
        # print(logs)
        if not logs or not len(logs):
            print_debug('0 commits in {}'.format(git_repo))
            continue
        commits = logs.split('\ncommit')
        print_debug('{} commits in {}'.format(len(commits), git_repo))
        if len(commits) == 0:
            print_debug('0 commits in {}'.format(git_repo))
            continue
        add_commits(commits, git_repo)


def update_releasenotes():
    global all_release_notes
    get_release_date()
    # init dict
    for module in modules:
        all_release_notes[bugfix][module] = []
        all_release_notes[feature][module] = []
    collect_commit()
    validate_all_commits()
    save_release_notes()


def count_line_of_commit():
    """
    count line of commit for all
    :return:
    """
    get_release_date(skip_versions=1)
    line_added_total = line_deleted_total = 0
    for git_repo in git_repos_for_line:
        line_added = line_deleted = 0
        cmds = 'cd {}; git log --since={}  --pretty=tformat: --numstat'.format(path_root + git_repo,
                                                                               '-'.join(last_release_date))
        print_debug(cmds)
        logs = os.popen(cmds).read()
        if len(logs):
            for log in logs.split('\n'):
                if not log:
                    continue
                # print(log)
                items = log.split('\t')
                line_added += int(items[0])
                line_deleted += int(items[1])
        if line_added + line_deleted:
            line_added_total += line_added
            line_deleted_total += line_deleted
            print_debug('{:5d} lines added, {:5d} lines deleted, in repo {}'.format(line_added, line_deleted, git_repo))
    print('\nAdded   {:5d} lines.\nDeleted {:5d} lines.\nTotal   {:5d} lines.'.format(line_added_total, line_deleted_total, line_added_total + line_deleted_total))


def validate_repo_sync():
    """
    Check whether the code is latest
    :return:
    """
    print('Check repo status...')
    no_diff = 1
    cmd = 'cd {0};{1};cd {0};{2}'.format(path_root, cmd_repo_sync, cmd_repo_status)
    print_debug(cmd)
    out = os.popen(cmd)
    for t in out.readlines():
        print(t, end='')
        if no_diff and t[5:7] == ' -':
            no_diff = 0
    if not no_diff:
        print('')
        print('{:10}{}'.format('[WARNING]', 'You SHOULD have your code consistent with repo,'
                                            ' unless you know what you are doing! And keep archiving...'))


def archive_common_before():
    global path_archive_tmp_root
    if not len(path_archive_tmp_root):
        path_archive_tmp_root = path_root_parent + dir_root_readonly + '_for_archive_tmp/'
    validate_repo_sync()
    get_version()
    for cmds in cmds_archive_common_before:
        os_popen(cmds.format(path_root, dir_root_readonly, path_archive_tmp_root))


def archive_common_after():
    for cmds in cmds_archive_common_after:
        os_popen(cmds.format(path_root, dir_root_readonly, path_archive_tmp_root))


def archive_test():
    archive_common_before()
    for cmds in cmds_archive_test:
        os_popen(cmds.format(path_archive_tmp_root, version_internal_new, dir_root_readonly, path_root_parent), 'w')
    archive_common_after()


def archive_release():
    archive_common_before()
    for cmds in cmds_archive_release:
        os_popen(cmds.format(path_archive_tmp_root, version_internal_new, dir_root_readonly, path_root_parent), 'w')
    archive_common_after()


def chdir_home():
    global path_root, path_root_parent, dir_version_readonly, cmd_repo_sync
    if path_root in os.listdir(os.getcwd()):
        path_root_parent = os.getcwd() + '/'
    else:
        pwd = str(os.getcwd())
        index = pwd.find(path_root)
        if index < 0:
            print('{:10}can NOT find dir \'{}\' in pwd, exiting...\n'.format('[Error]', path_root))
            exit(0)
        path_root_parent = pwd[:index]
    path_root = path_root_parent + path_root + '/'
    cmd_repo_sync = cmd_repo_sync.format(path_root)
    dir_version_readonly = path_root + dir_version_readonly
    print_debug(path_root_parent)
    print_debug(dir_version_readonly)
    print_debug(path_root)
    print_debug(cmd_repo_sync)


def check_environment():
    if sys.platform.find('linux') < 0:
        print('{:10} platform is {} Not linux. Need bash to run sed, tar and so on. Exiting...\n'.format(
            '[Error]', sys.platform))
        exit(0)


def dispatcher(n):
    if n == 0:
        show_menu()
    elif n == 1:
        update_version()
        # No use to include 'update version' info in ReleaseNotes.
        # So no use to merge 'update version' info before update_releasenotes()
        update_releasenotes()
        print('{:10}{}'.format('[WARNING]', 'Now, quit. You SHOULD check changes in \'platform/bsp/*\' and '
                                            '\'tools/build/ReleaseNotes\' and merge them. '
                                            'Then rerun this script and do following jobs.'))
        print('{:10}{}\n'.format('\n[OK]', menus[n]))
        exit(0)
    elif n == 2:
        archive_test()
    #elif n == 3:
    #    archive_release() # use test for release
    elif n == 3:
        count_line_of_commit()


def main():
    check_environment()
    chdir_home()
    show_menu()
    while 1:
        try:
            n = get_option()
            assert n >= 0 and n < len(menus)
            print('\n{:10}{} {}  {}'.format('*' * 8, n + 1, menus[n], '*' * 8))
            if n == len(menus) - 1:
                print('{:10}{}\n'.format('\n[OK]', menus[n]))
                break
            dispatcher(n)
            print('{:10}{}\n'.format('\n[OK]', menus[n]))
        except (ValueError, AssertionError):
            print_debug(sys.exc_info()[0])
            print_debug(sys.exc_info()[2].tb_lineno)
            if n != -1:
                print('{:10}{}\n'.format('\n[Error]', menus[n]))


def parse_argv():
    global debug, print_number
    if len(sys.argv) >= 2:
        debug = 1
        print_number = print_number


if __name__ == '__main__':
    parse_argv()
    main()
